﻿using System;
using System.Collections.Generic;
using System.IO;

namespace DAPNet
{
    public class Wave
    {
        private const int RiffchunkIDValue = 0x46464952;
        
        private const int WaveFormatValue = 0x45564157;

        private const int FmtSubchunk1IDValue = 0x20746D66;

        private const int PcmSubchunk1SizeValue = 16;

        private const Int16 PcmAudioFormatValue = 1;

        private const int MinNumChannels = 1;

        private const int MinSampleRate = 1;
        
        private const int DataSubchunk2IDValue = 0x61746164;

        private const int MinSubchunk2Size = 0;

        private const int BitsInByte = 8;

        private SampleConverterFactory sampleConverterFactory;

        private Boolean isValid;

        private int chunkSize;

        private Int16 numChannels;

        private int sampleRate;

        private int byteRate;

        private Int16 blockAlign;

        private Int16 bitDepth;

        private int numSamplesPerChannel;

        private int subchunk2Size;

        private List<SampleCollection> channels;

        private List<Byte> informationalData;


        public Wave() : this(new DefaultSampleConverterFactory())
        { 
        }

        public Wave(SampleConverterFactory sampleConverterFactory)
        {
            this.sampleConverterFactory = sampleConverterFactory;
            isValid = false;
            chunkSize = 0;
            numChannels = 0;
            sampleRate = 0;
            byteRate = 0;
            blockAlign = 0;
            bitDepth = 0;
            numSamplesPerChannel = 0;
            subchunk2Size = 0;
            channels = new List<SampleCollection>();
            informationalData = new List<Byte>();
        }

        public int NumChannels
        {
            get 
            { 
                return numChannels; 
            }
        }

        public int SampleRate
        {
            get 
            { 
                return sampleRate; 
            }
        }

        public int NumSamplesPerChannel
        {
            get 
            { 
                return numSamplesPerChannel; 
            }
        }

        public SampleCollection this[int channelIndex]
        {
            get 
            { 
                return channels[channelIndex]; 
            }
        }

        public Wave Clone()
        {
            Wave clone = new Wave();
            clone.isValid = isValid;
            clone.chunkSize = chunkSize;
            clone.numChannels = numChannels;
            clone.sampleRate = sampleRate;
            clone.byteRate = byteRate;
            clone.blockAlign = blockAlign;
            clone.bitDepth = bitDepth;
            clone.numSamplesPerChannel = numSamplesPerChannel;
            clone.subchunk2Size = subchunk2Size;

            foreach (SampleCollection channel in channels)
            {
                clone.channels.Add(channel.Clone());
            }

            foreach (Byte infoDataByte in informationalData)
            {
                clone.informationalData.Add(infoDataByte);
            }

            return clone;
        }

        public void Read(string path)
        {
            isValid = false;
            BinaryReader waveReader = new BinaryReader(File.Open(path, FileMode.Open));
            ReadRiffChunkDescriptor(waveReader);
            ReadFmtSubchunk(waveReader);
            ReadDataSubchunk(waveReader);
            waveReader.Close();
            isValid = true;
        }

        public void Write(string path)
        {
            if (!isValid)
            {
                throw new InvalidOperationException("Represented wave is not valid.");
            }

            BinaryWriter waveWriter = new BinaryWriter(File.Open(path, FileMode.Create));
            WriteRiffChunkDescriptor(waveWriter);
            WriteFmtSubchunk(waveWriter);
            WriteDataSubchunk(waveWriter);
            waveWriter.Close();
        }

        private void ResetChannels()
        {
            this.channels.Clear();
            for (int channelIndex = 0; channelIndex < numChannels; channelIndex++)
            {
                this.channels.Add(new SampleCollection(numSamplesPerChannel));
            }
        }


        #region Chunk read methods
        private void ReadRiffChunkDescriptor(BinaryReader waveReader)
        {
            ReadChunkID(waveReader);
            ReadChunkSize(waveReader);
            ReadFormat(waveReader);
        }

        private void ReadFmtSubchunk(BinaryReader waveReader)
        {
            ReadSubchunk1ID(waveReader);
            ReadSubchunk1Size(waveReader);
            ReadAudioFormat(waveReader);
            ReadNumChannels(waveReader);
            ReadSampleRate(waveReader);
            ReadByteRate(waveReader);
            ReadBlockAllign(waveReader);
            ReadBitsPerSample(waveReader);
        }

        private void ReadDataSubchunk(BinaryReader waveReader)
        {
            ReadSubchunk2ID(waveReader);
            ReadSubchunk2Size(waveReader);
            ReadSamplesData(waveReader);
            ReadInformationalData(waveReader);
        }
        #endregion

        #region Chunk-part read methods
        private void ReadChunkID(BinaryReader waveReader)
        {
            int chunkIDValue = waveReader.ReadInt32();
            if (chunkIDValue != RiffchunkIDValue)
            {
                throw new FormatException("The ChunkID does not contain 'RIFF'.");
            }
        }

        private void ReadChunkSize(BinaryReader waveReader)
        {
            chunkSize = waveReader.ReadInt32();
        }

        private void ReadFormat(BinaryReader waveReader)
        {
            int formatValue = waveReader.ReadInt32();
            if (formatValue != WaveFormatValue)
            {
                throw new FormatException("The Format does not contain 'WAVE'.");
            }
        }

        private void ReadSubchunk1ID(BinaryReader waveReader)
        {
            int subchunk1ID = waveReader.ReadInt32();
            if (subchunk1ID != FmtSubchunk1IDValue)
            {
                throw new FormatException("The Subchunk1ID does not contain 'fmt '.");
            }
        }

        private void ReadSubchunk1Size(BinaryReader waveReader)
        {
            int subchunk1Size = waveReader.ReadInt32();
            if (subchunk1Size != PcmSubchunk1SizeValue)
            {
                throw new FormatException("The 'fmt' sub-chunk has not 24 byte size.");
            }
        }

        private void ReadAudioFormat(BinaryReader waveReader)
        {
            int audioFormat = waveReader.ReadInt16();
            if (audioFormat != PcmAudioFormatValue)
            {
                throw new FormatException("The audio format is not PCM.");
            }
        }

        private void ReadNumChannels(BinaryReader waveReader)
        {
            numChannels = waveReader.ReadInt16();
            if (numChannels < MinNumChannels)
            {
                throw new FormatException(String.Format("The NumChannels is less than {0}.", MinNumChannels));
            }
        }

        private void ReadSampleRate(BinaryReader waveReader)
        {
            sampleRate = waveReader.ReadInt32();
            if (sampleRate < MinSampleRate)
            {
                throw new FormatException(String.Format("The SampleRate is less than {0}.", MinSampleRate));
            }
        }

        private void ReadByteRate(BinaryReader waveReader)
        {
            byteRate = waveReader.ReadInt32();
        }

        private void ReadBlockAllign(BinaryReader waveReader)
        {
            blockAlign = waveReader.ReadInt16();
        }

        private void ReadBitsPerSample(BinaryReader waveReader)
        {
            bitDepth = waveReader.ReadInt16();
            SampleConverter sampleConverter = sampleConverterFactory.GetSampleConverter(bitDepth);
            if (sampleConverter == null)
            {
                throw new FormatException("The sample converter for given BitsPerSample is not implemented.");
            }

            float requiredByteRate = sampleRate * numChannels * bitDepth / BitsInByte;
            if (requiredByteRate != byteRate)
            {
                throw new FormatException("The ByteRate is not equal to 'SampleRate * NumChannels * BitsPerSample/8'.");
            }

            float requiredBlockAllign = numChannels * bitDepth / BitsInByte;
            if (requiredBlockAllign != blockAlign)
            {
                throw new FormatException("The BlockAllign is not equal to 'NumChannels * BitsPerSample/8'.");
            }
        }


        private void ReadSubchunk2ID(BinaryReader waveReader)
        {
            int subchunk2IDValue = waveReader.ReadInt32();
            if (subchunk2IDValue != DataSubchunk2IDValue)
            {
                throw new FormatException("The Subchunk2ID does not contain 'data'.");
            }
        }

        private void ReadSubchunk2Size(BinaryReader waveReader)
        {
            subchunk2Size = waveReader.ReadInt32();
            if (subchunk2Size < MinSubchunk2Size)
            {
                throw new FormatException(String.Format("The Subchunk2Size is less than {0}.", MinSubchunk2Size));
            }

            int bytesPerSample = bitDepth / BitsInByte;
            float numSamples = subchunk2Size / bytesPerSample;
            if (numSamples != Math.Floor(numSamples))
            {
                throw new FormatException("The Subchunk2Size is not divisible by bytes per sample.");
            }

            float numSamplesPerChannel = numSamples / numChannels;
            if (numSamplesPerChannel != Math.Floor(numSamplesPerChannel))
            {
                throw new FormatException("Number of samples is not divisible by NumChannels.");
            }
            this.numSamplesPerChannel = (int)numSamplesPerChannel;
        }

        private void ReadSamplesData(BinaryReader waveReader)
        {
            SampleConverter sampleConverter = sampleConverterFactory.GetSampleConverter(bitDepth);
            ResetChannels();
            for (int multiSampleIndex = 0; multiSampleIndex < this.numSamplesPerChannel; multiSampleIndex++)
            {
                for (int channelIndex = 0; channelIndex < numChannels; channelIndex++)
                {
                    channels[channelIndex][multiSampleIndex] = sampleConverter.ReadSample(waveReader);
                }
            }
        }

        private void ReadInformationalData(BinaryReader waveReader)
        {
            informationalData.Clear();
            while (waveReader.BaseStream.Position != waveReader.BaseStream.Length)
            {
                informationalData.Add(waveReader.ReadByte());
            }
        }
        #endregion

        #region Chunk write methods
        /// <summary>
        /// Writes the RiffChunkDescriptor of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the RiffChunkDescriptor.</param>
        private void WriteRiffChunkDescriptor(BinaryWriter waveWriter)
        {
            WriteChunkID(waveWriter);
            WriteChunkSize(waveWriter);
            WriteFormat(waveWriter);
        }

        /// <summary>
        /// Writes the FmtSubchunk of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the FmtSubchunk.</param>
        private void WriteFmtSubchunk(BinaryWriter waveWriter)
        {
            WriteSubchunk1ID(waveWriter);
            WriteSubchunk1Size(waveWriter);
            WriteAudioFormat(waveWriter);
            WriteNumChannels(waveWriter);
            WriteSampleRate(waveWriter);
            WriteByteRate(waveWriter);
            WriteBlockAlign(waveWriter);
            WriteBitsPerSample(waveWriter);
        }

        /// <summary>
        /// Writes the DataSubchunk of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the DataSubchunk.</param>
        private void WriteDataSubchunk(BinaryWriter waveWriter)
        {
            WriteSubchunk2ID(waveWriter);
            WriteSubchunk2Size(waveWriter);
            WriteSamplesData(waveWriter);
            WriteInfoData(waveWriter);
        }
        #endregion

        #region Chunk-part write methods
        /// <summary>
        /// Writes the ChunkID of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the ChunkID.</param>
        private void WriteChunkID(BinaryWriter waveWriter)
        {
            waveWriter.Write(RiffchunkIDValue);
        }

        /// <summary>
        /// Writes the ChunkSize of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the ChunkSize.</param>
        private void WriteChunkSize(BinaryWriter waveWriter)
        {
            waveWriter.Write(chunkSize);
        }

        /// <summary>
        /// Writes the Format of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Format.</param>
        private void WriteFormat(BinaryWriter waveWriter)
        {
            waveWriter.Write(WaveFormatValue);
        }


        /// <summary>
        /// Writes the Subchunk1ID of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Subchunk1ID.</param>
        private void WriteSubchunk1ID(BinaryWriter waveWriter)
        {
            waveWriter.Write(FmtSubchunk1IDValue);
        }

        /// <summary>
        /// Writes the Subchunk1Size of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Subchunk1Size.</param>
        private void WriteSubchunk1Size(BinaryWriter waveWriter)
        {
            waveWriter.Write(PcmSubchunk1SizeValue);
        }

        /// <summary>
        /// Writes the AudioFormat of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the AudioFormat.</param>
        private void WriteAudioFormat(BinaryWriter waveWriter)
        {
            waveWriter.Write(PcmAudioFormatValue);
        }

        /// <summary>
        /// Writes the NumChannels of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the NumChannels.</param>
        private void WriteNumChannels(BinaryWriter waveWriter)
        {
            waveWriter.Write(numChannels);
        }

        /// <summary>
        /// Writes the SampleRate of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the SampleRate.</param>
        private void WriteSampleRate(BinaryWriter waveWriter)
        {
            waveWriter.Write(sampleRate);
        }

        /// <summary>
        /// Writes the ByteRate of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the ByteRate.</param>
        private void WriteByteRate(BinaryWriter waveWriter)
        {
            waveWriter.Write(byteRate);
        }

        /// <summary>
        /// Writes the BlockAlign of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the BlockAlign.</param>
        private void WriteBlockAlign(BinaryWriter waveWriter)
        {
            waveWriter.Write(blockAlign);
        }

        /// <summary>
        /// Writes the BitsPerSample of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the BitsPerSample.</param>
        private void WriteBitsPerSample(BinaryWriter waveWriter)
        {
            waveWriter.Write(bitDepth);
        }


        /// <summary>
        /// Writes the Subchunk2ID of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Subchunk2ID.</param>
        private void WriteSubchunk2ID(BinaryWriter waveWriter)
        {
            waveWriter.Write(DataSubchunk2IDValue);
        }

        /// <summary>
        /// Writes the Subchunk2Size of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Subchunk2Size.</param>
        private void WriteSubchunk2Size(BinaryWriter waveWriter)
        {
            waveWriter.Write(subchunk2Size);
        }

        /// <summary>
        /// Writes the samples data of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Wave's samples data.</param>
        private void WriteSamplesData(BinaryWriter waveWriter)
        {
            SampleConverter sampleFilter = sampleConverterFactory.GetSampleConverter(bitDepth);
            for (int multiSampleIndex = 0; multiSampleIndex < numSamplesPerChannel; multiSampleIndex++)
            {
                for (int channelindex = 0; channelindex < numChannels; channelindex++)
                {
                    sampleFilter.WriteSample(waveWriter, channels[channelindex][multiSampleIndex]);
                }
            }
        }

        /// <summary>
        /// Writes the informational data of the Wave to the Wave writer.
        /// </summary>
        /// <param name="waveWriter">The Wave writer positioned at the Wave's informational data.</param>
        private void WriteInfoData(BinaryWriter waveWriter)
        {
            foreach (Byte infoByte in informationalData)
            {
                waveWriter.Write(infoByte);
            }
        }
        #endregion
    }
}
